package cn.jtcoding.gateway.filter;

import cn.jtcoding.gateway.auth.AuthDetail;
import cn.jtcoding.gateway.auth.AuthProperties;
import cn.jtcoding.gateway.auth.AuthSigner;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * @author jason.tang
 * @date 2021/4/9 13:39
 * @desc 鉴权过滤器
 * 参考百度智能云对API请求进行签名计算 <li>https://cloud.baidu.com/doc/Reference/s/Njwvz1wot<li/>
 *
 * 为什么要进行签名计算？
 * 签名过程会通过以下方式保护您的API请求：
 *  1. 验证请求者的身份
 *      签名可以确保请求是由某个具有有效访问密钥的用户或服务发起。
 *  2. 保护传输中的数据，防止非法篡改
 *      若请求在传输过程中遭到非法篡改，由于第三方无法对篡改后的请求进行计算，得到新的认证字符串(Authorization)，收到请求后认证字符串匹配将失败，因此身份校验无法通过。
 *  3. 防止重放攻击
 *      认证字符串(Authorization)都具有指定的有效时间。如请求被截获，第三方无法在有效时间之外重放请求。
 *
 * 签名流程
 *  1. 名词解释
 *  认证字符串(authorization): 非匿名API请求中必须携带的认证信息，也是最终API中要求的Authorization字段。由 “前缀字符串” 、“签名头域” 及 “签名摘要” 拼接而成。
 *  前缀字符串(authStringPrefix): 认证字符串的前缀部分，用于生成派生签名密钥(signingKey)和最终认证字符串的拼接
 *  签名头域(signedHeaders)：加入签名算法的HTTP头域列表，为认证字符串的中间部分
 *  规范请求(canonicalRequest)：经过规范化处理后的请求信息，又称待签名串。
 *  派生签名密钥(signingKey)：使用SK和前缀字符串生成派生签名密钥，然后使用派生密钥对规范请求生成最终签名摘要
 *  签名摘要(signature)：使用派生签名密钥对规范请求进行HMAC算法计算得到，为最终认证字符串的后半部分
 *
 *  2. 组成部分
 *   认证字符串：{prefix}/{accessKeyId}/{timestamp}/{expiredSeconds}/{signedHeaders}/{signature}
 *   前缀字符串：{prefix}/{accessKeyId}/{timestamp}/{expiredSeconds}
 *   签名头域：{signedHeaders}, 即签名算法中涉及到的HTTP头域列表
 *   签名摘要：{signature}, 计算所得
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    @Autowired
    private AuthProperties authProperties;

    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (!authProperties.getEnabled()) {
            return chain.filter(exchange);
        }

        ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getURI().getPath();
        String requestQuery = request.getURI().getQuery();
        String requestUri = Joiner.on("?").skipNulls().join(requestPath, requestQuery);
        if (this.permitAll(requestUri)) {
            return chain.filter(exchange);
        }

        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders headers = request.getHeaders();
        String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);
        if (StringUtils.isBlank(authorization)) {
            log.info("auth is empty.");
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        List<String> authDetails = Splitter.on("/").omitEmptyStrings().splitToList(authorization);
        if (authDetails.size() != 6) {
            log.info("auth length[{}] verify failure. auth: {}", authDetails.size(), authorization);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        AuthDetail authDetail = new AuthDetail(authDetails);
        if (!verifyAuthPrefix(authDetail)) {
            log.info("auth prefix verify failure. auth: {}", authorization);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        if (!verifySignedHeaders(authDetail.getSignedHeaders())) {
            log.info("auth signed headers verify failure. auth: {}", authorization);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }


        String httpMethod = request.getMethodValue().toUpperCase();
        String uri = requestUri.isEmpty() ? "/" : requestUri;
        List<String> headersOpt = Splitter.on(";").splitToList(authDetail.getSignedHeaders());

        AuthSigner authSigner = new AuthSigner(authDetail.getPrefix(), authDetail.getSignedHeaders(), authDetail.getExpiredSeconds(), authProperties.getSecretKey());
        String targetAuth = authSigner.sign(httpMethod, uri,  headers.toSingleValueMap(), headersOpt);
        if (!targetAuth.equals(authorization)) {
            log.info("Authorization mismatch.\nCanonicalRequest: {},\nAuthorization in header is: {},\ntarget is: {}",
                    authSigner.canonicalRequest(httpMethod, uri, headers.toSingleValueMap(), headersOpt),
                    authorization, targetAuth);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        return chain.filter(exchange);
    }

    private boolean verifySignedHeaders(String signedHeaders) {
        authProperties.getIncludeHeaders().sort(Comparator.comparing(v -> v));
        String targetSignedHeaders = Joiner.on(";").join(authProperties.getIncludeHeaders());
        return Objects.equals(signedHeaders, targetSignedHeaders);
    }

    private boolean verifyAuthPrefix(AuthDetail authDetail) {
        Integer expiredSeconds = authDetail.getExpiredSeconds();
        boolean verify = Objects.equals(authDetail.getPrefix(), authProperties.getPrefix())
                && Objects.equals(authDetail.getAccessKey(), authProperties.getAccessKey())
                && Objects.equals(expiredSeconds, authProperties.getExpiredSeconds());

        // 校验时间戳(单位：毫秒)
        long fromTimestamp = authDetail.getTimestamp();
        long currentTimestamp = System.currentTimeMillis();
        boolean reqIsExpired = currentTimestamp - fromTimestamp < expiredSeconds * 1000L;

        return verify && reqIsExpired;
    }

    private boolean permitAll(String requestUri) {
        return authProperties.getIgnoreUrlPatterns().stream()
                .anyMatch(r -> ANT_PATH_MATCHER.match(r, requestUri));
    }
}
